package oop.ex3.crosswords;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;

import oop.ex3.crosswords.CrosswordStructure.SlotType;
import oop.ex3.search.SearchBoardNode;

/**
 * An implementation of Crossword interface, <br>
 * this class that represent a Crossword Board
 * 
 * @author tsachiho
 * @author kobi_atiya
 * 
 */
public class MyCrossword implements Crossword {

    protected List<CrosswordEntry> movesRecord;      // provide move and undo-move
    protected MyCrosswordGlossary  glos;             // global glos
    protected MyCrosswordGlossary  usedGlos;         // used words glos
    protected MyCrosswordSlot[][]  puzzle;           // 2D array of 2Direction-Slots, represents the board game
    protected int                  quality;          // current board quality
    protected int                  sizeX;            // board x size
    protected int                  sizeY;            // board y size
    private int                    puzzleBound;      // board upper bound, based on slots potential
    private int                    upperQualityBound; // puzzle upper bound, based on unused words

    /**
     * Construct a new and empty board, needs to attachStructure and attachGlossary in order to use this board
     */
    public MyCrossword() {
        glos = new MyCrosswordGlossary();
        usedGlos = new MyCrosswordGlossary();
        puzzle = null;
        quality = 0;
        sizeX = 0;
        sizeY = 0;
        puzzleBound = 0;
        upperQualityBound = 0;
        movesRecord = new LinkedList<CrosswordEntry>();

    }

    /**
     * Copy Crossword from another one
     * 
     * @param other - the other crossword
     */
    public MyCrossword(MyCrossword other) {
        this.glos = new MyCrosswordGlossary(other.glos);
        this.usedGlos = new MyCrosswordGlossary(other.usedGlos);
        this.puzzle = new MyCrosswordSlot[other.sizeX][other.sizeY];
        // copy each slot separately
        for (int x = 0; x < other.sizeX; x++) {
            for (int y = 0; y < other.sizeY; y++) {
                if (other.puzzle[x][y] == null) puzzle[x][y] = null;
                else {
                    puzzle[x][y] = new MyCrosswordSlot(other.puzzle[x][y]);
                }
            }
        }
        this.quality = other.quality;
        this.sizeX = other.sizeX;
        this.sizeY = other.sizeY;
        this.puzzleBound = other.puzzleBound;
        this.upperQualityBound = other.upperQualityBound;
        this.movesRecord = new LinkedList<CrosswordEntry>(other.movesRecord);
    }

    @Override
    public boolean isBestSolution() {
        return getQuality() == Math.min(puzzleBound, glos.getQuality() + usedGlos.getQuality());
    }

    @Override
    public Iterator<CrosswordEntry> getMovesIterator() {
        upperQualityBound = quality; // recalculate upperQualityBound for each move
        List<CrosswordEntry> entries = new ArrayList<CrosswordEntry>();

        for (String term : glos.getTerms()) { 
            // copying already ordered legal moves for each term
            List<CrosswordEntry> termEntries = new ArrayList<CrosswordEntry>(legalMovesList(term));
            if (!termEntries.isEmpty()) {
                // if has legal moves, add them and update quality bound
                entries.addAll(termEntries);
                upperQualityBound += MyCrosswordGlossary.getlength(term);
            }

        }
        return entries.iterator();
    }

    @Override
    public int getQuality() {
        return quality;
    }

    @Override
    public int getQualityBound() {
        return upperQualityBound;
    }

    @Override
    public void doMove(CrosswordEntry move) {
        // update quality
        this.quality += move.getLength();

        // update puzzle
        int x = move.getPosition().getX();
        int y = move.getPosition().getY();
        puzzle[x][y].setEntry(move);

        // update glossaries 
        usedGlos.put(move.getTerm(), move.getDefinition());
        glos.remove(move.getTerm());

        // add move to moves records
        movesRecord.add(0, move);
    }

    @Override
    public void undoMove(CrosswordEntry move) {
        // Get most recent move
        CrosswordEntry lastMove = this.movesRecord.get(0);
        // Check if move is valid
        assert (move == lastMove);

        // Restore board
        int x = lastMove.getPosition().getX();
        int y = lastMove.getPosition().getY();
        int verSpace = puzzle[x][y].getSlotSize(Direction.VERTICAL);
        int horSpace = puzzle[x][y].getSlotSize(Direction.HORIZONTAL);
        puzzle[x][y] = new MyCrosswordSlot(x, y, verSpace, horSpace);

        // Update record
        this.movesRecord.remove(0);
        // update quality
        this.quality -= lastMove.getLength();
        // update glossaries
        usedGlos.remove(lastMove.getTerm());
        glos.put(lastMove.getTerm(), lastMove.getDefinition());
    }

    @Override
    public SearchBoardNode<CrosswordEntry> getCopy() {
        return new MyCrossword(this);
    }

    @Override
    public void attachGlossary(CrosswordGlossary glossary) {
        glos = new MyCrosswordGlossary((MyCrosswordGlossary) glossary);
    }

    @Override
    public void attachStructure(CrosswordStructure structure) {
        this.sizeX = structure.getWidth();
        this.sizeY = structure.getHeight();
        // creating puzzle in relevant size
        this.puzzle = new MyCrosswordSlot[sizeX][sizeY];

        // Creating each slot, by its hor & ver sizes
        for (int x = sizeX - 1; x >= 0; x--) {
            for (int y = sizeY - 1; y >= 0; y--) {
                // in case of "#" in puzzle - make it null
                if (structure.getSlotType(new MyCrosswordPosition(x, y, true)) == SlotType.FRAME_SLOT) {
                    puzzle[x][y] = null;
                }
                // in case of '_' in puzzle
                else {
                    int horSpace;
                    int verSpace;

                    // if there arn't spaces in the right of the slot (edge or '#')
                    if ((x == sizeX - 1) || (puzzle[x + 1][y] == null)) {
                        horSpace = 1;
                    } else { // add right slot size to this size +1
                        horSpace = puzzle[x + 1][y].getSlotSize(Direction.HORIZONTAL) + 1;
                    }

                    // if there arn't spaces in the bottom of the slot (edge or '#')
                    if ((y == sizeY - 1) || (puzzle[x][y + 1] == null)) {
                        verSpace = 1;

                    } else { // add bottom slot size to this size +1
                        verSpace = puzzle[x][y + 1].getSlotSize(Direction.VERTICAL) + 1;
                    }

                    // creating the slot
                    puzzle[x][y] = new MyCrosswordSlot(x, y, verSpace, horSpace);
                    puzzleBound += horSpace + verSpace; // update PuzzleUpperBound
                }
            }
        }
    }

    @Override
    public Collection<CrosswordEntry> getCrosswordEntries() {
        // Goes over the puzzle and coping the entries
        List<CrosswordEntry> entries = new LinkedList<CrosswordEntry>();
        for (int x = 0; x < sizeX; x++) {
            for (int y = 0; y < sizeY; y++) {
                if (puzzle[x][y] == null) continue; // prevent null reference exception
                // only if there is a term in entry
                if (puzzle[x][y].getEntry(Direction.VERTICAL).getTerm() != null)
                    entries.add(puzzle[x][y].getEntry(Direction.VERTICAL));
                // only if there is a term in entry
                if (puzzle[x][y].getEntry(Direction.HORIZONTAL).getTerm() != null)
                    entries.add(puzzle[x][y].getEntry(Direction.HORIZONTAL));
            }
        }

        return entries;
    }

    public String toString() {
        String output = "";
        for (int y = 0; y < sizeY; y++) {
            for (int x = 0; x < sizeX; x++) {
                output += getChar(x, y);
            }
            output += "\n";
        }
        return output;
    }

    /**
     * Return a list of legal entries for a given term, ordered by ex. description (overlapping, and entry
     * ordering)
     *  //O((V^3+H^3) + (V^3+H^3)log((V^3+H^3)))
     * @param term - a given term
     * @return list of legal moves
     */
    private List<MyCrosswordEntry> legalMovesList(String term) {
        List<MyCrosswordEntry> termEntries = new ArrayList<MyCrosswordEntry>();
        for (int y = 0; y < sizeY; y++) {
            for (int x = 0; x < sizeX; x++) {
                // if its '#', continue
                if (puzzle[x][y] == null) continue;

                // if term can be placed in slot
                if (puzzle[x][y].getEntry(Direction.VERTICAL) != null
                        && puzzle[x][y].getSlotSize(Direction.VERTICAL) >= term.length()) {
                    // get slot text and convert it to regex-friendly pattern
                    String pattern = getSlotText(puzzle[x][y], Direction.VERTICAL);
                    Pattern match = Pattern.compile(patternMaker(pattern));
                    if (match.matcher(term).matches()) {
                        MyCrosswordEntry verEntry = new MyCrosswordEntry(term, glos.getTermDefinition(term),
                                new MyCrosswordPosition(x, y, true)); // create entry
                        verEntry.setOverlapping(overlappingAmount(term, pattern)); // test and update overlapping
                        termEntries.add(verEntry);
                    }
                }

                // same as above, for Horizontal direction,
                if (puzzle[x][y].getEntry(Direction.HORIZONTAL) != null
                        && puzzle[x][y].getSlotSize(Direction.HORIZONTAL) >= term.length()) {
                    String pattern = getSlotText(puzzle[x][y], Direction.HORIZONTAL);
                    Pattern match = Pattern.compile(patternMaker(pattern));
                    if (match.matcher(term).matches()) {
                        MyCrosswordEntry horEntry = new MyCrosswordEntry(term, glos.getTermDefinition(term),
                                new MyCrosswordPosition(x, y, false));
                        horEntry.setOverlapping(overlappingAmount(term, pattern));
                        termEntries.add(horEntry);
                    }

                }
            }
        }
        // sorting all legal entries by Comparator (base on overlapping and other ex3 description
        Collections.sort(termEntries, new MyCrosswordEntryComparator<MyCrosswordEntry>());
        return termEntries;
    }

    /**
     * Return the char from a given coordinate
     * 
     * @param x - x coordinate
     * @param y - y coordinate
     * @return - the char at the coordinate
     */
    private char getChar(int x, int y) {
        if (x >= sizeX || y >= sizeY) return '?';

        /* Horizontal-line scanning */
        if (puzzle[x][y] == null) return '#';
        for (int iX = 0; iX <= x; iX++) {
            if (puzzle[iX][y] == null) continue;
            if (puzzle[iX][y].getEntry(Direction.HORIZONTAL) == null) continue;
            if (puzzle[iX][y].getEntry(Direction.HORIZONTAL).getTerm() == null) continue;
            if (puzzle[iX][y].getEntry(Direction.HORIZONTAL).getTerm().length() > (x - iX)) {
                return puzzle[iX][y].getEntry(Direction.HORIZONTAL).getTerm().charAt(x - iX);
            }
        }
        /* Vertical-line scanning */
        for (int iY = 0; iY <= y; iY++) {
            if (puzzle[x][iY] == null) continue;
            if (puzzle[x][iY].getEntry(Direction.VERTICAL) == null) continue;
            if (puzzle[x][iY].getEntry(Direction.VERTICAL).getTerm() == null) continue;
            if (puzzle[x][iY].getEntry(Direction.VERTICAL).getTerm().length() > (y - iY)) {
                return puzzle[x][iY].getEntry(Direction.VERTICAL).getTerm().charAt(y - iY);
            }
        }
        // if there is no char in coordinate
        return '_';
    }

    /**
     * Return the current slot text, letters will be showed, empty spaces will showed as '_'
     * 
     * @param slot - the wanted slot text
     * @param dir - the wanted direction
     * @return the current slot text
     */
    private String getSlotText(MyCrosswordSlot slot, Direction dir) {
        int x = slot.getEntry(dir).getPosition().getX();
        int y = slot.getEntry(dir).getPosition().getY();
        String pattern = "";
        // goes over the slot chars and getting the chars
        for (int i = 0; i < slot.getSlotSize(dir); i++) {
            switch (dir) {
                case HORIZONTAL:
                    pattern += getChar(x + i, y);
                    break;

                case VERTICAL:
                    pattern += getChar(x, y + i);
                    break;
            }
        }
        return pattern;
    }

    /**
     * Return a regex-friendly pattern<br>
     * each '_' will replaced by '\w' for regex search<br>
     * all '_' in the end of the document will be replaced with {0,COUNT} for pattern matching
     * 
     * @param pattern - the pre-made pattern
     * @return the regex-friendly pattern
     */
    private String patternMaker(String pattern) {
        int counter = 0;
        if (pattern.endsWith("_")) {
            while (pattern.endsWith("_")) {
                pattern = pattern.substring(0, pattern.length() - 1);
                counter++;
            }
            pattern += "_{0," + counter + "}";
        }
        pattern = pattern.replaceAll("_", "\\\\w");
        return pattern;
    }

    /**
     * returns the amount of overlapping letters<br>
     * assumes pattern is from the wanted coordinate, <br>
     * <br>
     * for example:<br>
     * term: "net" ,pattern: "internet", result = 0 <br>
     * term: "demo", pattern: [mo]"dem__", result = 3
     * 
     * @param term - the given term to search for
     * @param pattern - the given pattern
     * @return amount of overlapping letters
     */
    private static int overlappingAmount(String term, String pattern) {
        if (pattern.startsWith(term)) return 0;

        if (pattern.contains(term)) return term.length();

        int overlappingCounter = 0;
        for (int i = 0; i < term.length(); i++) {
            if (term.charAt(i) == pattern.charAt(i)) {
                overlappingCounter++;
            } else if (pattern.charAt(i) == '_') {
                continue;
            } else return 0;
        }
        return overlappingCounter;

    }
}